﻿using System;
using System.Collections.Generic;
using Microsoft.SharePoint;
using System.Collections;

namespace Alcero.SharePoint.Data
{
    /// <summary>
    /// A wrapper around an SPList to be able to use it as a Data Storage object.
    /// The DataStore object stores serialized object information in the list's underlying folder structure.
    /// </summary>
    public class DataStore
    {
        private String _webUrl;

        private List<Object> _storeKeys = new List<object>();
        private Dictionary<Object, Guid> _keyToStoreGuidMap = new Dictionary<object, Guid>();

        private static Dictionary<String, DataStore> dataStores = new Dictionary<String, DataStore>();
        private const String DATASTORE_NAME = "DataStoreList";
        private const String ROOT_CONTAINER_NAME = "StoreData";

        //This is used to have a user wait for a store if a load call was previously requested by another user.
        //used by Thread Monitor class to notify of store loading.
        private static List<string> _storesCurrentlyLoading = new List<string>();

        private static object _lockObj = new object();

        /// <summary>
        /// Give the original SPWeb passed in parameter. Used by extended methods.
        /// </summary>
        public SPWeb Web
        {
            get
            {
                if (string.IsNullOrEmpty(_webUrl))
                {
                    return null;
                }
                else
                {
                    SPWeb web = null;

                    using (SPSite site = new SPSite(_webUrl))
                    {
                        using (SPWeb tempWeb = site.OpenWeb())
                        {
                            web = tempWeb;
                        }
                    }

                    return web;
                }
            }
        }

        private DataStore(SPWeb web)
        {
            using (SPWeb elevatedWeb = Utility.GetElevatedWeb(web))
            {
                this._webUrl = web.Url;
                bool listExisted = true;
                SPList list;

                //yea it looks stupid but there is no way of checking if a list exists prior to fetching it... so we go with ugly try/catch block.
                try
                {
                    list = elevatedWeb.Lists[DATASTORE_NAME];
                }
                catch (Exception)
                {
                    listExisted = false;
                    this.CreateDataStore(elevatedWeb);
                    dataStores.Add(this._webUrl, this);
                }

                if (listExisted)
                {
                    this.LoadStoreData(); //loads all the keys in the datastore.
                    dataStores.Add(this._webUrl, this);
                }
            }
        }

        /// <summary>
        /// Get the number of elements in the datastore.
        /// </summary>
        public int Size
        {
            get { return this._storeKeys.Count; }
        }

        /// <summary>
        /// This method loads all the related store data into memory. Obviously this might take some time.
        /// </summary>
        private void LoadStoreData()
        {
            using (SPWeb web = Utility.GetElevatedWeb(this._webUrl))
            {
                SPList storeList = web.Lists[DATASTORE_NAME];

                //Get all our folders.
                SPFolderCollection allContainers = storeList.RootFolder.SubFolders[ROOT_CONTAINER_NAME].SubFolders;

                foreach (SPFolder container in allContainers)
                {
                    SPFile key = container.Files["k"];
                    SPFile storeData = container.Files["sd"];

                    //Fetch the store data info object.
                    byte[] storeDataBytes = storeData.OpenBinary();
                    String storeString = Utility.UTF8ByteArrayToString(storeDataBytes);
                    StoreDataInfo storeInfo = Utility.BinaryDeserializeObject(storeString) as StoreDataInfo;

                    if (storeInfo != null)
                    {
                        //Fetch the key info object.
                        byte[] keyDataBytes = key.OpenBinary();
                        String keyXmlString = Utility.UTF8ByteArrayToString(keyDataBytes);
                        Object keyObject = Utility.BinaryDeserializeObject(keyXmlString);

                        //Update our memory reference objects.
                        this._storeKeys.Add(keyObject);
                        try
                        {
                            this._keyToStoreGuidMap.Add(keyObject, new Guid(container.Name));
                        }
                        catch { }
                    }
                }
            }
        }

        /// <summary>
        /// Get all the keys contained in the store.
        /// </summary>
        public ICollection Keys
        {
            get
            {
                return this._keyToStoreGuidMap.Keys;
            }
        }

        /// <summary>
        /// Retreive the data store object related with the web.
        /// </summary>
        /// <param name="web">The web object from which to fetch the data store.</param>
        /// <returns></returns>
        public static DataStore GetStore(SPWeb web)
        {
            DataStore store = null;

            //DataStore was previously loaded and is available in momory.
            if (dataStores.ContainsKey(web.Url))
            {
                return dataStores[web.Url];
            }
            else
            {
                lock (_lockObj)
                {
                    if (dataStores.ContainsKey(web.Url))
                    {
                        return dataStores[web.Url];
                    }
                    else
                    {
                        store = new DataStore(web);
                    }
                }
            }

            return store;
        }


        public Boolean IsProvisionning()
        {
            if (_storesCurrentlyLoading.Contains(this._webUrl))
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// An a new entry to the data store.
        /// </summary>
        /// <param name="key">The key (Must be a serializable object)</param>
        /// <param name="data">The data (Must be an serializable object)</param>
        public void Add(Object key, Object data)
        {
            //Some validation checks before we proceed.
            if (this._storeKeys.Contains(key))
            {
                throw new ArgumentException("An object with the same key has already been added.");
            }

            //Make sure the objects are both serialzable.
            if (!(key.GetType().IsSerializable) || !(data.GetType().IsSerializable))
            {
                throw new ArgumentException("One of the objects being added to the store is not serializable.");
            }

            using (SPWeb web = Utility.GetElevatedWeb(this._webUrl))
            {
                SPList storeList = web.Lists[DATASTORE_NAME];

                //Add our container folder to the data folder.
                Guid containerId = Guid.NewGuid();
                storeList.ParentWeb.AllowUnsafeUpdates = true;
                SPFolder containerFolder = storeList.RootFolder.SubFolders[ROOT_CONTAINER_NAME].SubFolders.Add(containerId.ToString());
                storeList.ParentWeb.AllowUnsafeUpdates = false;

                //Create store data files
                this.CreateStoreFiles(containerFolder);

                StoreDataInfo storeData = new StoreDataInfo();
                storeData.DataDataTypeString = data.GetType().AssemblyQualifiedName;
                storeData.KeyDataTypeString = key.GetType().ToString();

                String serializedData = String.Empty;
                byte[] byteData;

                //create the store data file.
                SPFile storeDataFile = containerFolder.Files["sd"];
                serializedData = Utility.BinarySerializeObject(storeData);
                byteData = Utility.StringToUTF8ByteArray(serializedData);
                storeDataFile.ParentFolder.ParentWeb.AllowUnsafeUpdates = true;
                storeDataFile.SaveBinary(byteData);
                storeDataFile.ParentFolder.ParentWeb.AllowUnsafeUpdates = false;

                //create the key file
                SPFile keyFile = containerFolder.Files["k"];
                serializedData = Utility.BinarySerializeObject(key);
                byteData = Utility.StringToUTF8ByteArray(serializedData);
                keyFile.ParentFolder.ParentWeb.AllowUnsafeUpdates = true;
                keyFile.SaveBinary(byteData);
                keyFile.ParentFolder.ParentWeb.AllowUnsafeUpdates = false;

                //create the data file
                SPFile dataFile = containerFolder.Files["d"];
                serializedData = Utility.BinarySerializeObject(data);
                byteData = Utility.StringToUTF8ByteArray(serializedData);
                dataFile.ParentFolder.ParentWeb.AllowUnsafeUpdates = true;
                dataFile.SaveBinary(byteData);
                dataFile.ParentFolder.ParentWeb.AllowUnsafeUpdates = false;

                //Add to memory store.
                this._storeKeys.Add(key);
                this._keyToStoreGuidMap.Add(key, containerId);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="container"></param>
        private void CreateStoreFiles(SPFolder container)
        {
            container.ParentWeb.AllowUnsafeUpdates = true;
            container.Files.Add("k", new byte[0]);
            container.Files.Add("d", new byte[0]);
            container.Files.Add("sd", new byte[0]);
            container.ParentWeb.AllowUnsafeUpdates = false;
        }


        /// <summary>
        /// Remove the store entry with this key.
        /// </summary>
        /// <param name="Key"></param>
        public void Remove(Object Key)
        {
            if (this._storeKeys.Contains(Key))
            {
                using (SPWeb web = Utility.GetElevatedWeb(this._webUrl))
                {
                    SPList storeList = web.Lists[DATASTORE_NAME];

                    Guid containerGuid = this._keyToStoreGuidMap[Key];
                    storeList.ParentWeb.AllowUnsafeUpdates = true;
                    storeList.RootFolder.SubFolders[ROOT_CONTAINER_NAME].SubFolders.Delete(containerGuid.ToString());
                    storeList.ParentWeb.AllowUnsafeUpdates = false;

                    //update memory objects
                    this._storeKeys.Remove(Key);
                    this._keyToStoreGuidMap.Remove(Key);
                }
            }
            else
            {
                throw new ArgumentException("No such key in the data store.");
            }
        }

        private Guid GetContainerForKey(Object key)
        {
            return this._keyToStoreGuidMap[key];
        }

        /// <summary>
        /// Get the store data info file from the container id (folder guid)
        /// </summary>
        /// <param name="container">the sp folder object representing the container for this entry.</param>
        /// <returns></returns>
        private StoreDataInfo GetStoreDataInfo(SPFolder container)
        {
            SPFile storeData = container.Files["sd"];
            container.ParentWeb.AllowUnsafeUpdates = true;
            //Fetch the store data info object.
            byte[] storeDataBytes = storeData.OpenBinary();
            String storeXmlString = Utility.UTF8ByteArrayToString(storeDataBytes);
            StoreDataInfo storeInfo = Utility.BinaryDeserializeObject(storeXmlString) as StoreDataInfo;
            container.ParentWeb.AllowUnsafeUpdates = false;
            return storeInfo;
        }

        /// <summary>
        /// Get the data object for this container.
        /// </summary>
        /// <param name="containerId"></param>
        /// <returns></returns>
        private Object GetDataObject(Guid containerId)
        {
            Object dataObject = null;

            using (SPWeb web = Utility.GetElevatedWeb(this._webUrl))
            {
                SPList storeList = web.Lists[DATASTORE_NAME];

                SPFolder container = this.GetContainer(containerId, storeList);
                container.ParentWeb.AllowUnsafeUpdates = true;
                StoreDataInfo info = this.GetStoreDataInfo(container);

                //create the data file
                SPFile dataFile = container.Files["d"];

                //Fetch teh data object.
                byte[] dataBytes = dataFile.OpenBinary();
                String dataXmlString = Utility.UTF8ByteArrayToString(dataBytes);
                dataObject = Utility.BinaryDeserializeObject(dataXmlString);
                container.ParentWeb.AllowUnsafeUpdates = false;

            }

            return dataObject;
        }

        /// <summary>
        /// Check to see if the store has a key matching <para>key</para>
        /// </summary>
        /// <param name="key">The key to match.</param>
        /// <returns></returns>
        public Boolean ContainsKey(Object key)
        {
            if (this._storeKeys.Contains(key))
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Fetch the data object matching <para>key</para>
        /// </summary>
        /// <param name="key">The key to fetch the data from.</param>
        /// <returns></returns>
        public Object this[Object key]
        {
            get
            {
                if (this._storeKeys.Contains(key))
                {
                    return this.GetDataObject(this._keyToStoreGuidMap[key]);
                }

                return null;
            }
            set
            {
                if (this._storeKeys.Contains(key))
                {
                    this.UpdateDataObject(key, value);
                }
                else
                {
                    throw new ArgumentException("The key specified was not found in the data store.");
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        private SPFolder GetContainer(Guid id, SPList storeList)
        {
            if (storeList.RootFolder.SubFolders[ROOT_CONTAINER_NAME].SubFolders != null)
            {
                try
                {
                    bool exists = storeList.RootFolder.SubFolders[ROOT_CONTAINER_NAME].SubFolders[id.ToString()].Exists;
                }
                catch (NullReferenceException)
                {
                    //folder was not found?
                    return null;
                }
            }

            return storeList.RootFolder.SubFolders[ROOT_CONTAINER_NAME].SubFolders[id.ToString()];
        }

        /// <summary>
        /// Update the data object in the data store.
        /// </summary>
        /// <param name="Key"></param>
        /// <param name="data"></param>
        private void UpdateDataObject(Object Key, Object data)
        {
            using (SPWeb web = Utility.GetElevatedWeb(this._webUrl))
            {
                SPList storeList = web.Lists[DATASTORE_NAME];

                //create the data file
                SPFolder container = this.GetContainer(this.GetContainerForKey(Key), storeList);

                container.ParentWeb.AllowUnsafeUpdates = true;

                SPFile dataFile = container.Files["d"];
                String serializedData = Utility.BinarySerializeObject(data);
                byte[] byteData = Utility.StringToUTF8ByteArray(serializedData);
                dataFile.SaveBinary(byteData);
                container.ParentWeb.AllowUnsafeUpdates = false;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="web"></param>
        private void CreateDataStore(SPWeb web)
        {
            web.AllowUnsafeUpdates = true;
            Guid listId = web.Lists.Add(DATASTORE_NAME, "Data Store - DO NOT MANUALLY EDIT, DELETE OR MODIFY THIS LIST IN ANY WAY. DATA LOSS MAY OCCUR. FOR INTERNAL USE ONLY.", SPListTemplateType.GenericList);

            SPList list = web.Lists[listId];

            //List options
            list.AllowDeletion = false;
            list.Hidden = true;
            list.OnQuickLaunch = false;

            list.RootFolder.SubFolders.Add(ROOT_CONTAINER_NAME);
            //Assign security, breaking inheritance and assigning 0 security will only allow access to the system account, which is what we want.
            list.BreakRoleInheritance(false);
            list.ParentWeb.AllowUnsafeUpdates = true;
            list.Update();
            list.ParentWeb.AllowUnsafeUpdates = false;
            web.AllowUnsafeUpdates = false;
        }
    }

    /// <summary>
    /// A class used to store information about data store objects.
    /// </summary>
    [Serializable]
    public class StoreDataInfo
    {
        private String _keyDataType;
        private String _dataDataType;

        public StoreDataInfo()
        {
            //
        }

        public String KeyDataTypeString
        {
            get { return this._keyDataType; }
            set { this._keyDataType = value; }
        }

        public String DataDataTypeString
        {
            get { return this._dataDataType; }
            set { this._dataDataType = value; }
        }

        public Type DataDataType
        {
            get { return Type.GetType(_dataDataType, false, true); }
        }

        public Type KeyDataType
        {
            get { return Type.GetType(_keyDataType, false, true); }
        }

    }
}
